#!/usr/bin/env python
# -*- coding: utf-8 -*-

import logging
logging.basicConfig(level=logging.INFO)

import os
import time
import uuid
import base64
import hashlib
import inspect
from datetime import datetime

import web

from mako.lookup import TemplateLookup
from mako import exceptions

__author__ = 'Michael Liao'

import memcache

memcache_client = None
try:
    memcache_client = memcache.Client(['127.0.0.1:11211'], debug=0)
except:
    pass

def cache(timeout=3600):
    '''
    using decorator:
      @cache(300)
      def get_poem(arg1, arg2):
          return 'success'
    
    is equal to:
      def get_poem(arg1, arg2):
          return 'success'
      poem = cache(300)(get_poem)
    '''
    def _wrapper(func):
        def _new_func(*args, **kw):
            global memcache_client
            if memcache_client is None:
                return func(*args, **kw)
            key = '%s(%s)' % (func.__name__, '-'.join([str(arg) for arg in args]))
            logging.info('memcache key: %s' % key)
            r = memcache_client.get(key)
            if r:
                logging.info('memcache hit.')
                web.header('X-Cache', 'HIT from memcached')
                return r
            logging.info('memcache miss.')
            web.header('X-Cache', 'MISS from memcached')
            r = func(*args, **kw)
            memcache_client.set(key, r, timeout)
            return r
        _new_func.__name__ = func.__name__
        return _new_func
    return _wrapper

class emptyobject(object):
    def __getattr__(self, attr):
        return ''

    def __setattr__(self, attr, value):
        pass

class odict(dict):
    def __getattr__(self, attr):
        return self[attr]

    def __setattr__(self, attr, value):
        self[attr] = value

class WebError(StandardError):
    def __init__(self, message):
        super(WebError, self).__init__(message)

def next_id():
    return uuid.uuid4().hex

def connect():
    return web.database( \
            dbn = 'mysql', \
            host = 'localhost', \
            db = 'tinykb', \
            user = 'tinykb', \
            pw = 'tinykb')

TEMPLATE_PATH = os.path.join(os.path.split(os.path.abspath(__file__))[0], 'templates')
logging.info('Init template path: %s' % TEMPLATE_PATH)

TEMPLATES_LOOKUP = TemplateLookup(directories=[TEMPLATE_PATH], input_encoding='utf-8', output_encoding='utf-8')

def render(templ, model):
    template = TEMPLATES_LOOKUP.get_template(templ)
    return template.render(**model)

def handler(method='GET', contentType=None):
    '''
    using decorator:
      @handler('GET')
      def login(arg1, arg2):
          return 'success'
    
    is equal to:
      def login(arg1, arg2):
          return 'success'
      login = handler('GET')(login)
    '''
    def _wrapper(func):
        def _new_func(*args, **kw):
            if method=='GET' and web.ctx.method!='GET':
                raise web.badrequest()
            if method=='POST' and web.ctx.method!='POST':
                raise web.badrequest()
            if not contentType:
                web.header('Content-Type', contentType)
            r = func(*args, **kw)
            logging.info('Url handler returns: %s' % type(r))
            if r is None or method=='POST':
                return r
            if isinstance(r, dict):
                try:
                    if 'title' not in r:
                        r['title'] = func.__name__
                    if '__include__' not in r:
                        r['__include__'] = func.__name__ + '.html'
                    r['strftime'] = lambda x: datetime.strftime(datetime.fromtimestamp(x / 1000), '%Y-%m-%d %H:%M')
                    r['customer'] = web.ctx.customer
                    r['user'] = web.ctx.user
                    logging.info('Model: %s' % str(r))
                    return render('__main__.html', r)
                except:
                    return exceptions.html_error_template().render()
            if isinstance(r, basestring):
                return r
            raise ValueError('Expect return type None, dict or basestring.')
        argspec = inspect.getargspec(func)
        num_args = len(argspec[0])
        has_varargs = argspec[1] is not None
        num_defaults = 0
        if argspec[3]:
            num_defaults = len(argspec[3])
        _new_func.min_args = num_args - num_defaults
        _new_func.max_args = 20 if has_varargs else num_args
        _new_func.__name__ = func.__name__
        _new_func.handler = True
        return _new_func
    return _wrapper

COOKIE_NAME = 'tinykbuser'
COOKIE_EXPIRES = 31536000

_COOKIE_SALT = 'TinyKB@OpenSource'

def create_oauth_cookie(id, expires, email):
    s = '%s:%s:%s:%s' % (str(id), expires, str(email), _COOKIE_SALT)
    logging.info('make md5(%s)' % s)
    md5 = hashlib.md5(s).hexdigest()
    return base64.b64encode('%s:%s:%s' % (id, expires, md5))

def extract_oauth_cookie(cookie_str):
    if not cookie_str:
        logging.info('no cookie found.')
        return None
    try:
        s = base64.b64decode(str(cookie_str))
        logging.info('decoded cookie: %s' % s)
    except TypeError:
        logging.info('Bad cookie: base64 decode error.')
        return None
    # id:expires:md5
    ss = s.split(':', 2)
    if len(ss)!=3:
        logging.info(r'Bad cookie: split ":" failed.')
        return None
    id = ss[0]
    try:
        expires = int(ss[1])
    except ValueError:
        logging.info('Bad cookie: invalid time: %s' % ss[1])
        return None
    if time.time() > expires:
        logging.info('Bad cookie: expired time.')
        return None
    md5 = str(ss[2])
    # check md5:
    users = list(web.ctx.db.select('kbuser', where='id=$id', vars=dict(id=id)))
    if users:
        user = users[0]
        s = '%s:%s:%s:%s' % (id, expires, str(user.email), _COOKIE_SALT)
        logging.info('make md5(%s)' % s)
        if md5 != hashlib.md5(s).hexdigest():
            logging.info('Bad cookie: invalid md5.')
            return None
        logging.info('Found user from cookie: id=%s' % id)
        return user
    logging.info('Bad cookie: user not found in db.')
    return None
